Skip to content

Conversation

@tluebeck
Copy link

@tluebeck tluebeck commented Jul 6, 2025

Which issue(s) are closed by this pull request?

Closes #165

Changes proposed in this pull request:

  • introduce SphericalHarmonicsAudio base class which holds all SH related properties
  • introduce SphericalHarmonicsTimeData and FrequencyData

@tluebeck tluebeck changed the base branch from main to develop July 6, 2025 13:36
@tluebeck tluebeck self-assigned this Jul 6, 2025
@tluebeck tluebeck added the enhancement New feature or request label Jul 6, 2025
@tluebeck tluebeck marked this pull request as draft July 6, 2025 13:37
@mberz mberz moved this from Backlog to Drafting Phase in Weekly Planning Jul 8, 2025
Copy link
Member

@f-brinkmann f-brinkmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the effort. It generally looks good to me! I suggested one structural change and adding the docs but otherwise its fine to me. I think the testing should be fine. complete functionality is tested for SH signal, initialization tested for each class separately.

Comment on lines 208 to 218
# check dimensions
if len(data.shape) < 3:
raise ValueError("Invalid number of dimensions. Data should have "
"at least 3 dimensions.")

# set n_max
n_max = np.sqrt(data.shape[-2])-1
if n_max - int(n_max) != 0:
raise ValueError("Invalid number of SH channels: "
f"{data.shape[-2]}. It must match (n_max + 1)^2.")
self._n_max = int(n_max)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a little redundant to me to have this in all three classes. Would the following work:

  • First call the init of the Audio class (FrequencyData in this case)
  • Second call the init of the SHAudio class and move this code there

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, yes that works.



class SphericalHarmonicTimeData(SphericalHarmonicsAudio, TimeData):
"""_summary_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docs - probably intended to get a review before the effort.



class SphericalHarmonicFrequencyData(SphericalHarmonicsAudio, FrequencyData):
"""_summary_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docs - probably intended to get a review before the effort.

@github-project-automation github-project-automation bot moved this from Drafting Phase to Require review in Weekly Planning Jul 10, 2025
tluebeck added 2 commits July 11, 2025 07:24
add docstrings
move n_max and dimensions check to base class
@tluebeck tluebeck requested a review from f-brinkmann July 18, 2025 11:14
Copy link
Member

@f-brinkmann f-brinkmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I would suggest to wait with the merge until we finalized discussions on the SH conventions that we want to support. This might cause changes here as well.

@github-project-automation github-project-automation bot moved this from Require review to Reviewer Approved in Weekly Planning Jul 24, 2025
@f-brinkmann f-brinkmann moved this from Reviewer Approved to Require review in Weekly Planning Jul 24, 2025
@ahms5 ahms5 added this to the v1.0.0 milestone Jul 25, 2025
Copy link
Member

@ahms5 ahms5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking care!
just minor comments

class SphericalHarmonicSignal(Signal):
"""Create audio object with spherical harmonics coefficients in time or
frequency domain.
class SphericalHarmonicAudio(_Audio):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class SphericalHarmonicAudio(_Audio):
class _SphericalHarmonicAudio(_Audio):

shouldnt this be private, too?

comment=comment, is_complex=is_complex)
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley, domain="time", comment=comment)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
condon_shortley, domain="time", comment=comment)
condon_shortley, domain=domain, comment=comment)

domain="time"would overwrite the given domain. But i think we can remove it here anyway

Comment on lines 52 to 54
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley, domain, comment=""):
_Audio.__init__(self, domain=domain, comment=comment)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley, domain, comment=""):
_Audio.__init__(self, domain=domain, comment=comment)
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley):

i wounder if we really need it here, because it would be set twice, by the audio class and then overwritten by this class. I think the _Audio.init is not setting anything else then this to entries, so i guess we can remove it.

Comment on lines 233 to 235
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley, domain="time", comment=comment)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley, domain="time", comment=comment)
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley)

@sikersten
Copy link
Member

Thanks for implementing! I think I am missing something fundamental here. Why is it not possible that _SphericalHarmonicAudio inherits from the SphericalHarmonics class?

@mberz
Copy link
Member

mberz commented Aug 22, 2025

Thanks for implementing! I think I am missing something fundamental here. Why is it not possible that _SphericalHarmonicAudio inherits from the SphericalHarmonics class?

Because the SphericalHarmonics class contains a number of properties which are not meaningful in the SphericalHarmonicSignal class. They however share a set of properties/methods which would make a shared base class usefull, see #173

@ahms5 ahms5 modified the milestones: v1.0.0, v1.1.0 Sep 12, 2025
@mberz
Copy link
Member

mberz commented Oct 23, 2025

Sorry, forgot one thing. I would suggest to rename audio.py to sh_audio.py.

Please rename files in this pr. this will cause conflicts with other branches.

I'm very sorry, I meant to write don't rename in this branch, since the file is already part of develop.
Could you please revert dfd4d54 ?

Copy link
Member

@mberz mberz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just commenting on this now, as I feel we need to make some adjustments to the SphericalHarmonicDefinition class before continuing with this PR.

  1. I think it makes sense to move the functionality of saving the old properties to the parent class, as it could come in handy in other classes.
  2. I now remember why I first had the SphericalHarmonicDefintion in mind without the n_max property defined. I'll add an abstract base class for that which could serve as parent for the SphericalHarmonicAudio classes.

Sorry to postpone this PR once again.

@f-brinkmann f-brinkmann self-requested a review November 26, 2025 17:15
Copy link
Member

@f-brinkmann f-brinkmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the question on slack, I think it would be cleaner to keep data out of _SHAudio. Could this work:

  • move checks for n_max from _SHAudio.__init__ to a private method, e.g., _SHAudio._check_data_shape. This avoids the need to pass data to init.
  • Call this private method in each setter, e.g., SHTimeData.time
  • In each init, e.g., SHTimeData first initialize _SHAudio, than call the private method for checking the data shape, and finally call TimeData.__init__

Other points:

Instead of inheriting from _SphericalHarmonicBase and re-implementing the n_max getter, I think it would be cleaner to inherit from SphericalHarmonicDefinition and

  • overload the n_max setter to raise a ValueError, e.g., 'n_max can not be set and is defined by the shape of the data contained in the spherical harmonic Audio object'
  • overload the basis_type setter to raise a ValueError, e.g., 'Setting the basis_type is not yet supported'. This could be implemented at a later point, if we want. Since we decided to internally store data in standard conventions, this would be fairly straigt forward in this case.

Copy link
Member

@mberz mberz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a couple of questions which may help to avoid the missing self._data problem.

condon_shortley):

# check dimensions
if len(self._data.shape) < 3:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(self._data.shape) < 3:
if len(self.cshape) < 2:

"at least 3 dimensions.")

# calculate n_max
n_max = np.sqrt(self._data.shape[-2])-1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
n_max = np.sqrt(self._data.shape[-2])-1
n_max = np.sqrt(self._data.cshape[-1])-1

raise ValueError("Invalid number of SH channels: "
f"{self._data.shape[-2]}. It must match "
"(n_max + 1)^2.")
self._n_max = int(n_max)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._n_max = int(n_max)

@property
def n_max(self):
"""Get or set the spherical harmonic order."""
return self._n_max
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return self._n_max
return int(np.sqrt(self._data.cshape[-1])-1)


class SphericalHarmonicSignal(Signal):
"""Create audio object with spherical harmonics coefficients in time or
class _SphericalHarmonicAudio(_Audio, _SphericalHarmonicBase):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class _SphericalHarmonicAudio(_Audio, _SphericalHarmonicBase):
class _SphericalHarmonicAudio(_Audio, _SphericalHarmonicBase, ABC):

FrequencyData.freq.fset(self, value)


class SphericalHarmonicSignal(_SphericalHarmonicAudio, Signal):
Copy link
Member

@sikersten sikersten Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I do not fully get the concept, but shouldn't this be

Suggested change
class SphericalHarmonicSignal(_SphericalHarmonicAudio, Signal):
class SphericalHarmonicSignal(SphericalHarmonicTimeData, SphericalHarmonicFrequencyData):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. With the new inheritation structure we need that. I have it changed locally already :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not obsolete, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup

Copy link
Member

@f-brinkmann f-brinkmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Structure and docs are fine to me. I found a few small things that still should be changed.

@property
def n_max(self):
"""Get or set the spherical harmonic order."""
return int(np.sqrt(self.cshape[-1])-1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think overwriting the setter for basis_type must still be done to raise an AttributeError along the lines of 'Changing the basis_type' is not yet supported'.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a closer look at that point. I tried this real quick, but if we overwrite it, we cannot init _SphericalHarmonicBase because it calls the basis_type setter :D Let me check if there is a way without changing _SphericalHarmonicBase.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a solution - not super beautiful but works

Adds a singleton dimensions at the front if necessary.
"""

return data[np.newaxis, ...] if data.ndim < 3 else data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this change, a 2D array could be returned if passing a 1D array:

Suggested change
return data[np.newaxis, ...] if data.ndim < 3 else data
return np.atleast_2d(data)[np.newaxis, ...] if data.ndim < 3 else data

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

FrequencyData.freq.fset(self, value)


class SphericalHarmonicSignal(_SphericalHarmonicAudio, Signal):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not obsolete, right?

def n_max(self):
"""Get the maximum spherical harmonic order."""
return self._n_max
def freq(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getter and setter for freq_raw must be overwritten as well, I think.

@pyfar pyfar deleted a comment from mberz Dec 4, 2025
@pyfar pyfar deleted a comment from mberz Dec 4, 2025
overwrite basis_type setter and freq_raw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Require review

Development

Successfully merging this pull request may close these issues.

SH Time and FrequencyData

6 participants